package com.selebash;

import com.selebash.config.Config;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import com.selebash.ast.AbstractAst;
import com.selebash.ast.JsonAst;
import com.selebash.exception.SelebashException;
import com.selebash.exception.SelebashParseException;
import com.selebash.features.FeatureInitProcessor;
import com.selebash.scripts.Script;
import java.io.File;
import java.io.IOException;
import java.util.Stack;
import org.apache.commons.io.FileUtils;

public class SelebashParser {

    private final Localization localization;
    private final ParserContext parserContext = new ParserContext();
    private final Config config;
    public static final String DefaultFunctionName = "DEFAULT_SELEBASH_ FUNCTION NAME";

    public SelebashParser(Config config) {
        this.localization = config.getLocalization();
        this.config = config;
        initFeatures();
    }

    private void initFeatures() {
        FeatureInitProcessor featureInitProcessor = new FeatureInitProcessor();
        featureInitProcessor.process(parserContext, config);
    }

    public Script parse(File file) {
        try {
            String str = FileUtils.readFileToString(file);
            return parse(str, file.getAbsolutePath());
        } catch (IOException ex) {
            throw new SelebashException("Cannot read file " + file.getAbsolutePath(), ex);
        }
    }

    public Script parse(String text, String fileName) {
        String srcWithoutComment = new CommentRemover().process(text, localization);
        ParserInputStream stream = new ParserInputStream(srcWithoutComment, localization);
        return parse(stream, fileName);
    }

    public static boolean wordEqual(String word1, String word2) {
        if (word1 == null || word2 == null) {
            return false;
        }

        return word1.equalsIgnoreCase(word2);
    }

    public Script parse(ParserInputStream stream, String fileName) {
        Script testCase = new Script();
        testCase.setLocalization(localization);
        stream.skipBlank();
        parserContext.setCurrentDocument(testCase);
        while (true) {
            int index = stream.getIndex();
            String word = stream.readWord();
            stream.skipBlank();
            if (wordEqual(word, localization.getString(Localization.STEPS))) {
                readTask(stream, testCase);
            } else if (wordEqual(word, localization.getString(Localization.IF))) {
                readIf(stream, testCase);
            } else if (wordEqual(word, localization.getString(Localization.DATA))) {
                readJson(stream, testCase);
            } else if (stream.peek(0) == ':') {
                readProperty(word, stream, testCase);
            } else {
                throw new SelebashParseException("Не определенная конструкция " + word, index, stream.getIndex());
            }

            stream.skipBlank();
            if (stream.eof() || word == null) {
                break;
            }
        }

        if (testCase.getName() == null) {
            throw new SelebashParseException(localization.getString(Localization.TEST_EXPECTED), 0, stream.getIndex());
        }

        return testCase;
    }

    private void readProperty(String propertyName, ParserInputStream stream, Script testCase) {
        propertyName = Utils.stripQuotes(propertyName);
        StringBuilder sb = new StringBuilder();
        int tabLevel = stream.getTabLevel();
        String testDescription = readStringProperty(stream);
        sb.append(testDescription);
        stream.skipBlank();
        while (stream.getTabLevel() >= (tabLevel + 1)) {
            sb.append("\n");
            testDescription = stream.readStringUntilEndOfLine();
            sb.append(testDescription);
            stream.skipBlank();
        }

        testCase.addProperty(propertyName, sb.toString());
    }

    private String readStringProperty(ParserInputStream stream) {
        char c = stream.next();
        if (c != ':') {
            throw new SelebashParseException(localization.getString(Localization.EXPECTED_COLON), stream.getIndex() - 1, stream.getIndex());
        }

        stream.skipBlank();
        return stream.readStringUntilEndOfLine();
    }

    private void readJson(ParserInputStream stream, Script testCase) {
        int functionstart = stream.getIndex();
        char c = stream.peek(0);
        String stepsName = null;
        if (c != ':') {
            stepsName = stream.readWord();
            if (stepsName == null) {
                String message = String.format(localization.getString(Localization.CHARACTER_IS_NOT_A_FALID_CHAR_FOR_STEP_NAME), c);
                throw new SelebashParseException(message, functionstart, functionstart);
            }

            stepsName = Utils.stripQuotes(stepsName).toLowerCase();
            stream.skipBlank();
            c = stream.peek(0);
        } else {
            throw new SelebashParseException(localization.getString(Localization.DATA_SHOULD_HAVE_NAME), stream.getIndex() - 1, stream.getIndex());
        }

        if (c != ':') {
            throw new SelebashParseException(localization.getString(Localization.EXPECTED_COLON), stream.getIndex() - 1, stream.getIndex());
        }

        stream.next();
        stream.skipBlank();
        c = stream.peek(0);
        if (c == '[' || c == '{') {
            JsonAst ast = readJson(stream);
            testCase.getAstNodes().put(stepsName, ast);
        } else {
            String message = String.format(localization.getString(Localization.DATA_SHOULD_START_WITH));
            throw new SelebashParseException(message, functionstart, stream.getIndex());
        }
    }

    private void readTask(ParserInputStream stream, Script testCase) {
        int functionstart = stream.getIndex();
        char c = stream.peek(0);
        String stepsName = null;
        if (c != ':') {
            stepsName = stream.readWord();
            if (stepsName == null) {
                String message = String.format(localization.getString(Localization.CHARACTER_IS_NOT_A_FALID_CHAR_FOR_STEP_NAME), c);
                throw new SelebashParseException(message, functionstart, functionstart);
            }
            stepsName = Utils.stripQuotes(stepsName).toLowerCase();
            stream.skipBlank();
            c = stream.peek(0);
        } else {
            stepsName = DefaultFunctionName;
        }

        if (c != ':') {
            throw new SelebashParseException(localization.getString(Localization.EXPECTED_COLON), stream.getIndex() - 1, stream.getIndex());
        }
        stream.next();

        stream.skipBlank();
        c = stream.peek(0);

        AbstractAst node = readFunction(stream);
        if (stepsName.equals(DefaultFunctionName)) {
            if (testCase.getAstNodes().get(DefaultFunctionName) != null) {
                String message = String.format(localization.getString(Localization.SHOULD_BE_ONLY_ONE_DEFAULT_STEPS));
                throw new SelebashParseException(message, functionstart, stream.getIndex());
            }
        }
        testCase.getAstNodes().put(stepsName, node);
    }

    private void readIf(ParserInputStream stream, Script testCase) {
        int functionstart = stream.getIndex();
        char c = stream.peek(0);
        String stepsName;
        if (c != ':') {
            stepsName = stream.readWord();
            if (stepsName == null) {
                String message = String.format(localization.getString(Localization.CHARACTER_IS_NOT_A_FALID_CHAR_FOR_STEP_NAME), c);
                throw new SelebashParseException(message, functionstart, functionstart);
            }
            stepsName = Utils.stripQuotes(stepsName).toLowerCase();
            stream.skipBlank();
            c = stream.peek(0);
        } else {
            stepsName = DefaultFunctionName;
        }

        if (c != ':') {
            throw new SelebashParseException(localization.getString(Localization.EXPECTED_COLON), stream.getIndex() - 1, stream.getIndex());
        }

        stream.next();
        stream.skipBlank();
        AbstractAst node = readFunction(stream);
        if (stepsName.equals(DefaultFunctionName)) {
            if (testCase.getAstNodes().get(DefaultFunctionName) != null) {
                String message = String.format(localization.getString(Localization.SHOULD_BE_ONLY_ONE_DEFAULT_STEPS));
                throw new SelebashParseException(message, functionstart, stream.getIndex());
            }
        }
        testCase.getAstNodes().put(stepsName, node);
    }

    private char getEndJsonSymbol(char c, int start, int end) {
        if (c == '{') {
            return '}';
        }
        if (c == '[') {
            return ']';
        }

        throw new SelebashParseException("Illegal json start block symbol", start, end);
    }

    private boolean isJsonStartSymbol(char c) {
        return c == '{' || c == '[';
    }

    private JsonAst readJson(ParserInputStream stream) {
        int prevIndex = stream.getIndex();
        StringBuilder sb = new StringBuilder();
        Stack<Character> nestingStack = new Stack<>();
        char startSymbol = stream.next();
        nestingStack.push(getEndJsonSymbol(startSymbol, prevIndex, stream.getIndex()));
        sb.append(startSymbol);
        while (true) {
            char c = stream.next();

            if (isJsonStartSymbol(c)) {
                nestingStack.push(getEndJsonSymbol(c, prevIndex, stream.getIndex()));
            } else if (nestingStack.peek() == c) {
                nestingStack.pop();
                if (nestingStack.empty()) {
                    sb.append(c);
                    break;
                }
            } else if (stream.eof()) {
                String message = String.format(localization.getString(Localization.JSON_IS_NOT_CLOSED), nestingStack.peek());
                throw new SelebashParseException(message, prevIndex, stream.getIndex());
            }

            if (c == '\r') {
                c = '\n';
            }

            sb.append(c);
        }

        JsonAst ast = new JsonAst();
        JsonParser parser = new JsonParser();
        JsonElement jsonElement;
        try {
            jsonElement = parser.parse(sb.toString());
        } catch (JsonSyntaxException ex) {
            String message = String.format(localization.getString(Localization.JSON_IS_NOT_PARSEABLE), ex.getMessage());
            throw new SelebashParseException(message, prevIndex, stream.getIndex());
        }

        int index = stream.getIndex();
        ast.setDocument(parserContext.getCurrentDocument());
        ast.setJson(jsonElement);
        ast.setStart(prevIndex);
        ast.setEnd(index);
        return ast;
    }

    private AbstractAst readFunction(ParserInputStream stream) {
        FunctionParser parser = new FunctionParser(parserContext);
        return parser.readSequence(stream, config);
    }
}
